package com.a0soft.gphone.aTruffleHog.di;
/*
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation; either version 3, or (at your option) any
 * later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * Copyright(C) 2011 
 *   Sam Lu <ysl.sam.lu (at) gmail.com>
 *   Alger Lin <addre75 (at) gmail.com>
 *
 */

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;
import java.util.Map.Entry;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

import org.apache.http.protocol.HTTP;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import com.a0soft.gphone.aTruffleHog.util.AESEncryptor;
import com.a0soft.gphone.aTruffleHog.util.CONSTS;
import com.a0soft.gphone.aTruffleHog.util.CrashHandler;
import com.a0soft.gphone.aTruffleHog.util.aDebug;

import android.content.Context;
import android.os.AsyncTask;
import android.os.Looper;
import android.text.TextUtils;

public class DeviceInfo {
    public interface OnListener {
        public abstract void OnDeviceInfoReady(DeviceInfo di);
    }
    private OnListener                 m_listener;
    
    private Context                    m_ctx;
    private HashMap<String, Object>	   m_mapINK2Value; //Object can be either String, JSONArray or JSONObject
    
    private ReadOrBuildDeviceInfoTask  m_task;
    private boolean m_bReadCompleted;
    
    private static DeviceInfo          m_inst;
    
    //private StringBuilder 				m_sb = new StringBuilder(1024);
    
    private static String DIR_NAME = CONSTS.DATA_DIR_NAME;
    private static String FILE_NAME = "di.gz";
    
    private static String LOG_TAG = DeviceInfo.class.getSimpleName();

	////////////////////////////////////////////////////////
	public static DeviceInfo
	GetInstance(Context ctx)
	{
		if (m_inst == null) {
			m_inst = new DeviceInfo(ctx);
		}
		
		return m_inst;
	}
	
	public final void
	Init(boolean bRebuild, OnListener listener)
	{
	    if (!bRebuild && _IsReadCompleted()) {
	        if (aDebug.ENABLED)
                aDebug.d(LOG_TAG, "Init(): _IsReadCompleted return true");
	        listener.OnDeviceInfoReady(this);
	        return;
	    }
	    
        m_listener = listener; //override the previous callback function if it has
        
	    if (m_task != null) {
	        if (aDebug.ENABLED)
	            aDebug.w(LOG_TAG, "Init(): building task is running!");
	        return;
	    }
	    
	    m_task = new ReadOrBuildDeviceInfoTask(bRebuild);
	    m_task.execute();
	}
	
	public final void
	Stop()
	{
	    m_listener = null;
	}
	
	private class ReadOrBuildDeviceInfoTask extends AsyncTask<Void, Void, Void> {
	    private boolean m_bRebuild;
	    
	    ReadOrBuildDeviceInfoTask(boolean bRebuild)
	    {
	        m_bRebuild = bRebuild;
	    }
	    
	    @Override protected void 
	    onPreExecute()
        {
	        m_bReadCompleted = false;
        }

        @Override protected Void 
        doInBackground(Void... objs) 
        {
            // we need to make sure the Looper has been initialized for this thread.
            // this is required for android.bluetooth.BluetoothAdapter.getDefaultAdapter()
            if (Looper.myLooper() == null) {
                Looper.prepare();
            }
            
            _Read(m_bRebuild);
            return null;
        }

        @Override protected void 
        onPostExecute(Void obj) 
        {
            m_bReadCompleted = true;
         
            if (aDebug.ENABLED)
                aDebug.d(LOG_TAG, "ReadOrBuildDeviceInfoTask:onPostExecute()");
            
            if (m_listener != null)
                m_listener.OnDeviceInfoReady(DeviceInfo.this);
            
            CrashHandler.SetDID(GetString(DeviceInfoKey.K.UR_ID));
            
            m_task = null;
            m_listener = null;
        }
    }
	
	public final boolean
	IsReady()
	{
	    return _IsReadCompleted();
	}
	
	private boolean
	_IsReadCompleted()
	{
        return m_bReadCompleted;
	}
	
	//this function will be called in a non-ui thread
    private synchronized void
    _Read(boolean bRebuild)
    {
        //read cached device information from file
        if (aDebug.ENABLED)
            aDebug.d(LOG_TAG, "_Read(): start");
   	
        boolean bGoldenChanged = GoldenInfo.GetInstance(m_ctx).IsGoldenChanged();
           
        if (bRebuild || bGoldenChanged) {
            _Rebuild();
        }
        else {
            _Read(m_ctx);
        }
   	
        if (aDebug.ENABLED)
            aDebug.d(LOG_TAG, "_Read(): finished");
    }
	
    private void
    _Rebuild()
    {
	    if (aDebug.ENABLED)
            aDebug.d(LOG_TAG, "_Rebuild()");
	    
	    m_mapINK2Value.clear();

        Put(DeviceInfoKey.K.AP_INK_VERSION, CONSTS.INK_VERSION);
        Put(DeviceInfoKey.K.AP_INK_CREATE_TIME, System.currentTimeMillis());
        
        CpuInfo.Build(this);
        DisplayInfo.Build(this);
        MemInfo.Build(this);
        SysInfo.Build(this);
        StorageInfo.Build(this);
        TeleInfo.Build(this);
        UserInfo.Build(this);
        CameraInfo.Build(this);
        SensorInfo.Build(this);
        WifiInfo.Build(this, m_ctx);
        BtInfo.Build(this, m_ctx);
        
        _CheckIfAllInfoSpecified();
        
        _Write(m_ctx);
    }
	
	public void
	ToJSONString(StringBuilder sb)
	{
	    _CheckIfAllInfoSpecified();
	    
		//TODO: can be improved without using JSONObject, directly write out JSON string instead
		JSONObject json = new JSONObject();
		Set<Entry<String, Object>> entries = m_mapINK2Value.entrySet();
        try {
    		for (Entry<String, Object> entry: entries) {
                json.put(entry.getKey(), entry.getValue());
    		}
            sb.append(json.toString());
        } catch (JSONException e) {
            if (aDebug.ENABLED)
            	aDebug.d(LOG_TAG, "ToJSONString()", e);
        }
	}
	
	public void
	Dump(StringBuilder sb)
	{
        _CheckIfAllInfoSpecified();
        
		Set<Entry<String, Object>> entries = m_mapINK2Value.entrySet();
		for (Entry<String, Object> entry: entries) {
			String strINK = entry.getKey();
			sb.append(DeviceInfoKey.GetName(strINK))
			  .append('(').append(strINK).append(')')
			  .append(": ")
			  .append(entry.getValue().toString())
			  .append('\n');
		}
	}
	
	public final boolean
	HasKey(String strINK)
	{
	    if (m_mapINK2Value == null) return false;
	    return m_mapINK2Value.containsKey(strINK);
	}
	
	public final String
	GetString(String strINK)
	{
	    if (aDebug.ENABLED) {
	        if (!_IsReadCompleted()) {
	            aDebug.e(LOG_TAG, "Please call Init() first! strINK=" + strINK);
	        }
	    }
	    
	    Object obj = m_mapINK2Value.get(strINK);
	    if (obj == null) 
	        return "";
	    else
	        return obj.toString();		
	}
	
	public final JSONArray
	GetJSONArray(String strINK)
	{
	    String strVal = GetString(strINK);
        if (TextUtils.isEmpty(strVal)) return null;
        
        try {
            return new JSONArray(strVal);
        } catch (JSONException e) {
            return null;
        }
	}
	
	public final long
	GetLong(String strINK)
	{
		String strVal = GetString(strINK);
		if (TextUtils.isEmpty(strVal)) return 0;
		
		try {
			return Long.parseLong(strVal);
		} catch (NumberFormatException e) {
			if (aDebug.ENABLED)
				aDebug.e(LOG_TAG, "GetLong(): " + strVal, e);
			return 0;
		}
	}
	
	public final int
	GetInt(String strINK)
	{
		String strVal = GetString(strINK);
		if (TextUtils.isEmpty(strVal)) return 0;
		
		try {
			return Integer.parseInt(strVal);
		} catch (NumberFormatException e) {
			if (aDebug.ENABLED)
				aDebug.e(LOG_TAG, "GetInt(): " + strVal, e);
			return 0;
		}
	}
	
	public final boolean
    GetBoolean(String strINK)
    {
        String strVal = GetString(strINK);
        if (TextUtils.isEmpty(strVal)) return false;
        
        return Boolean.parseBoolean(strVal);
    }
	
	final void
	Put(String strINK, String strValue)
	{
		m_mapINK2Value.put(strINK, strValue);
	}
	
	final void
	Put(String strINK, long lValue)
	{
		m_mapINK2Value.put(strINK, Long.toString(lValue));
	}
	
	final void
	Put(String strINK, int nValue)
	{
		m_mapINK2Value.put(strINK, Integer.toString(nValue));
	}
	
	final void
    Put(String strINK, boolean bValue)
    {
        m_mapINK2Value.put(strINK, Boolean.toString(bValue));
    }
	
	final void
    Put(String strINK, float fValue)
    {
        m_mapINK2Value.put(strINK, Float.toString(fValue));
    }
	
	final void
    Put(String strINK, double dxValue)
    {
        m_mapINK2Value.put(strINK, Double.toString(dxValue));
    }
	
	final void
	Put(String strINK, Collection<?> collections)
	{
		m_mapINK2Value.put(strINK, new JSONArray(collections));
	}
	
	private
	DeviceInfo(Context ctx)
	{
	    m_ctx = ctx;
		m_mapINK2Value = new HashMap<String, Object>(1024);
	}	
	
	private void
	_CheckIfAllInfoSpecified()
	{
	    if (!aDebug.ENABLED) return;
	    
	    Set<String> strKeys = DeviceInfoKey.GetKeys();
	    for (String strKey: strKeys) {
	        if (m_mapINK2Value.get(strKey) == null) {
	            aDebug.e(LOG_TAG, "_Check(): null value for " + strKey);
	        }
	    }
	}
	
	private void
	_Write(Context ctx)
	{
	    StringBuilder sb = new StringBuilder(8192);
	    ToJSONString(sb);
	    
	    new WriteTask(ctx, sb.toString()).execute();
	}
	
	private void
    _Read(Context ctx)
    {
	    //new ReadTask(ctx, this).execute();

	    //call read in main thread
	    _HandleReadData(ReadTask._Read(m_ctx));
    }
	
	private void
	_HandleReadData(String str)
	{
	    if (aDebug.ENABLED)
            aDebug.d(LOG_TAG, "_HandleReadData(): len=" + (str == null ? 0 : str.length()));
	    
	    if (TextUtils.isEmpty(str)) {
	        _Rebuild();
            return;
	    }
	    
	    try {
            JSONObject json = new JSONObject(str);
            int nINKVersion = json.getInt(DeviceInfoKey.K.AP_INK_VERSION);
            
            if (nINKVersion != CONSTS.INK_VERSION) {
                _Rebuild();
                return;
            }
            
            m_mapINK2Value.clear();
            
            @SuppressWarnings("unchecked") 
            Iterator<String> iter = json.keys();
            
            while (iter.hasNext()) {
                String strKey = iter.next();
                m_mapINK2Value.put(strKey, json.get(strKey));
            }
        }
        catch (JSONException e) {
            if (aDebug.ENABLED)
                aDebug.e(LOG_TAG, "_HandleReadData()", e);
            
            _Rebuild();
            return;
        }
	}
	
	////////////////////////////////////////////////////////////////////////
    private static class WriteTask extends AsyncTask<Void, Void, Boolean> {
        private Context m_ctx;
        private String m_str;
        
        WriteTask(Context ctx, String str)
        {
            m_ctx = ctx;
            m_str = str;
        }
        
        @Override protected Boolean 
        doInBackground(Void... objs) 
        {
            if (aDebug.ENABLED)
                aDebug.d(LOG_TAG, "WriteTask::doInBackground(): start");
            File fDir = m_ctx.getDir(DIR_NAME, Context.MODE_PRIVATE);
            
            //delete old "device.info" file
            File fFile = new File(fDir, "device.info");
            if (fFile.exists()) fFile.delete();
            
            fFile = new File(fDir, FILE_NAME);

            BufferedWriter bw = null;
            try {
                m_str = AESEncryptor.Encrypt(m_str, m_ctx.getPackageName());
                if (m_str == null) return false;
                
                bw = new BufferedWriter(new OutputStreamWriter(new GZIPOutputStream(new FileOutputStream(fFile)), HTTP.UTF_8), 4096);                
                bw.write(m_str);                
                bw.close();

                bw = null;
                
                if (aDebug.ENABLED)
                    aDebug.d(LOG_TAG, "WriteTask::doInBackground(): finished");
                return true;
            } catch (IOException e) {
                if (aDebug.ENABLED)
                    aDebug.d(LOG_TAG, "WriteTask::doInBackground():", e);
                
                if (bw != null) {
                    try {
                        bw.close();
                    }
                    catch (IOException e1) {
                        if (aDebug.ENABLED)
                            aDebug.d(LOG_TAG, "WriteTask::doInBackground():", e);
                    }
                }
                return false;
            }
        }

        @Override protected void 
        onPostExecute(Boolean result)
        {
            m_ctx = null;
            m_str = null;
        }
    }
    
    ////////////////////////////////////////////////////////////////////////
    private static class ReadTask extends AsyncTask<Void, Void, String> {
        private Context m_ctx;
        private DeviceInfo m_di;
        
        @SuppressWarnings("unused") 
        ReadTask(Context ctx, DeviceInfo di)
        {
            m_ctx = ctx;
            m_di = di;
        }
        
        @Override protected String 
        doInBackground(Void... objs) 
        {
            return _Read(m_ctx);
        }

        private static String
        _Read(Context ctx)
        {
            if (aDebug.ENABLED)
                aDebug.d(LOG_TAG, "_Read(): start");
            
            File fDir = ctx.getDir(DIR_NAME, Context.MODE_PRIVATE);
            File fFile = new File(fDir, FILE_NAME);
            if (!fFile.exists() || !fFile.canRead()) return null;
                        
            BufferedReader br = null;
            try {
                StringBuilder sb = new StringBuilder();
                
                br = new BufferedReader(new InputStreamReader(new GZIPInputStream(new FileInputStream(fFile)), HTTP.UTF_8), 4096);
                char[] chBuf = new char[1024];
                int n;
                while ((n = br.read(chBuf)) >= 0) {
                    sb.append(chBuf, 0, n);
                }
                br.close();
                br = null;
                
                if (aDebug.ENABLED)
                    aDebug.d(LOG_TAG, "_Read(): finished");
                return AESEncryptor.Decrypt(sb.toString(), ctx.getPackageName());
            } catch (IOException e) {
                if (aDebug.ENABLED)
                    aDebug.d(LOG_TAG, "_Read()", e);
                
                if (br != null) {
                    try {
                        br.close();
                    }
                    catch (IOException e1) {
                        if (aDebug.ENABLED)
                            aDebug.d(LOG_TAG, "_Read()", e);
                    }
                }
            }
            return null;            
        }
        
        @Override protected void 
        onPostExecute(String strResult)
        {
            if (strResult != null)
                m_di._HandleReadData(strResult);
            
            m_ctx = null;
            m_di = null;
        }
    }
}
